use std::collections::BTreeMap;
use std::fmt::Debug;
use std::panic::{RefUnwindSafe, UnwindSafe};
use std::path::Path;
use std::sync::MutexGuard;
use std::{env, panic};

use config::{Config, File, FileFormat, Value, ValueKind};
use proptest::prelude::*;

use crate::tests::fixtures::SERIAL_TEST;
use crate::Error;

pub(crate) const GENERAL_STRING: &str = r"[[:print:][:space:]]";
pub(crate) const IDENTIFIER_STRING: &str = concat!(
    r"[\p{Latin}\p{Cyrillic}\p{Greek}\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}\p{N}\p{P}\p{S}",
    "&&[^.]]*"
);
pub(crate) const FILE_STEM_STRING: &str = concat!(
    r"[\p{Latin}\p{Cyrillic}\p{Greek}\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}\p{N}\p{P}\p{S}",
    "&&[^/]]+"
);

/// Helper function that enforces that T and U are the same type.
pub(crate) fn enforce_types_are_equal<T: Sized>(_t: T, _u: T) {}

// Taken from https://stackoverflow.com/a/67433684
/// Changes the current working directory to the specified path.
pub(crate) fn with_cwd<F, P: AsRef<Path>>(path: P, closure: F)
where
    F: Fn() + UnwindSafe + RefUnwindSafe,
{
    let guard = lock_serial_test();
    let old_cwd = env::current_dir().expect("the current working directory should be accessible");
    env::set_current_dir(path).expect("given directory should be accessible");

    match panic::catch_unwind(closure) {
        Ok(_) => {
            env::set_current_dir(old_cwd)
                .expect("the previous working directory should be accessible");
        },
        Err(error) => {
            env::set_current_dir(old_cwd)
                .expect("the previous working directory should be accessible");
            drop(guard);
            panic::resume_unwind(error);
        },
    };
}

pub(crate) fn lock_serial_test() -> MutexGuard<'static, ()> {
    SERIAL_TEST.lock().unwrap_or_else(|e| e.into_inner())
}

pub(crate) fn assert_expected_result_or_error<T: Sized + PartialEq + Debug>(
    result: Result<T, Error>,
    expected_result: Option<T>,
    expected_error: Option<Error>,
) {
    if let Some(expected_error) = expected_error {
        assert_expected_error(result, expected_error);
    } else {
        assert_eq!(
            expected_result.expect("expected Some, got None"),
            result.expect("expected Ok, got Err")
        );
    }
}

pub(crate) fn assert_expected_error<T: Sized + PartialEq + Debug>(
    result: Result<T, Error>,
    expected_error: Error,
) {
    let error = result.expect_err("expected an Err, got Ok");
    assert_eq!(expected_error, error);
}

pub fn make_config_from_json(json_str: &str) -> Config {
    Config::builder()
        .add_source(File::from_str(json_str, FileFormat::Json))
        .build()
        .expect("the JSON string should be valid")
}

pub fn option_string_strategy() -> impl Strategy<Value = Option<String>> {
    prop_oneof![Just(None), GENERAL_STRING.prop_map(Some)]
}
pub fn nil_string_value_kind_strategy() -> impl Strategy<Value = ValueKind> {
    prop_oneof![
        Just(ValueKind::Nil),
        GENERAL_STRING.prop_map(ValueKind::String)
    ]
}

pub fn simple_value_kind_strategy() -> impl Strategy<Value = ValueKind> {
    prop_oneof![
        Just(ValueKind::Nil),
        any::<bool>().prop_map(ValueKind::Boolean),
        any::<i64>().prop_map(ValueKind::I64),
        any::<i128>().prop_map(ValueKind::I128),
        any::<u64>().prop_map(ValueKind::U64),
        any::<u128>().prop_map(ValueKind::U128),
        any::<f64>().prop_map(ValueKind::Float),
        GENERAL_STRING.prop_map(ValueKind::String),
    ]
}

pub fn value_kind_strategy() -> impl Strategy<Value = ValueKind> {
    simple_value_kind_strategy().prop_recursive(3, 50, 7, |inner| {
        prop_oneof![
            prop::collection::vec(inner.clone(), 0..7).prop_map(|vector| ValueKind::Array(
                vector
                    .iter()
                    .map(|kind| Value::new(None, kind.clone()))
                    .collect()
            )),
            prop::collection::hash_map(IDENTIFIER_STRING, inner, 0..8).prop_map(|hash_map| {
                ValueKind::Table(
                    hash_map
                        .iter()
                        .map(|(key, kind)| (key.clone(), Value::new(None, kind.clone())))
                        .collect(),
                )
            }),
        ]
    })
}

pub fn arbitrary_btree_map_strategy() -> impl Strategy<Value = BTreeMap<String, ValueKind>> {
    prop::collection::btree_map(IDENTIFIER_STRING, value_kind_strategy(), 0..5)
}

pub fn arbitrary_json_value_strategy() -> impl Strategy<Value = serde_json::Value> {
    let leaf = prop_oneof![
        Just(serde_json::Value::Null),
        any::<bool>().prop_map(serde_json::Value::Bool),
        any::<f64>().prop_map(|number| serde_json::Value::Number(
            serde_json::Number::from_f64(number).unwrap()
        )),
        GENERAL_STRING.prop_map(serde_json::Value::String),
    ];
    leaf.prop_recursive(6, 16, 7, |inner| {
        prop_oneof![
            // Take the inner strategy and make the two recursive cases.
            prop::collection::vec(inner.clone(), 0..4).prop_map(serde_json::Value::Array),
            prop::collection::hash_map(IDENTIFIER_STRING, inner, 0..4).prop_map(|map| {
                serde_json::Value::Object(serde_json::map::Map::from_iter(map))
            }),
        ]
    })
}

pub fn arbitrary_json_object_strategy() -> impl Strategy<Value = serde_json::Value> {
    prop::collection::hash_map(IDENTIFIER_STRING, arbitrary_json_value_strategy(), 0..4)
        .prop_map(|map| serde_json::Value::Object(serde_json::map::Map::from_iter(map)))
}
